﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Muel.TypeScriptFacade.Core
{
    internal class TypeScriptObjectFinder
    {
        private TypeScriptObjectManager objectManager;
        
        /// <summary>
        /// The queue of objects to search
        /// </summary>
        private List<TypeScriptType> queue = new List<TypeScriptType>();

        /// <summary>
        /// The objects which have already been searched
        /// </summary>
        private List<TypeScriptType> searched = new List<TypeScriptType>();

        /// <summary>
        /// The objects which were searched and accepted
        /// </summary>
        private List<TypeScriptType> accepted = new List<TypeScriptType>();

        public TypeScriptObjectFinder(TypeScriptObjectManager typeScriptObjectManager)
        {
            this.objectManager = typeScriptObjectManager;
        }

        /// <summary>
        /// If the type parameter has types inheriting from it, or contains any properties which 
        /// types inheriting from them, or the types of the properties have properties with types
        /// inheriting from them (and so on recursively) then this will find all possible types
        /// of concrete classes which could be returned by the service layer
        /// </summary>
        /// <param name="returnType"></param>
        /// <returns></returns>
        internal TypeScriptType[] FindRelatedConcreteTypes(TypeScriptType type)
        {
            if (type == null)
            {
                throw new ArgumentNullException();
            }

            // ensure all objects in the TypeScriptObjectManager have been fully initialised
            this.objectManager.EnsureInitialised();

            // clear everything
            queue.Clear();
            searched.Clear();
            accepted.Clear();

            // add the first object to the queue
            AddToQueue(type);
            int iterationCounter = 0;
            do
            {
                // build up a list of types to queue up, adding relevant ones to the accepted list
                List<TypeScriptType> typesToQueue = new List<TypeScriptType>();
                foreach (TypeScriptType obj in queue)
                {
                    TypeScriptType[] nextBatch = FindRelevantDescendentTypes(obj);
                    foreach (TypeScriptType nextBatchObject in nextBatch)
                    {
                        if (!IsHandled(nextBatchObject))
                        {
                            typesToQueue.Add(nextBatchObject);
                        }
                        if (!accepted.Contains(nextBatchObject))
                        {
                            accepted.Add(nextBatchObject);
                        }
                    }
                }

                // move the queue to searched, and add the next batch to the queue
                searched.AddRange(queue);
                queue.Clear();
                foreach (TypeScriptType typeToQueue in typesToQueue)
                {
                    AddToQueue(typeToQueue);
                }

                iterationCounter++;
            } while (queue.Count > 0 && iterationCounter < 1000);

            if (queue.Count > 0)
            {
                throw new Exception("While finding related concrete types TSFacade went through over 1000 iterations and gave up");
            }

            return accepted.ToArray();
        }

        private void AddToQueue(TypeScriptType obj)
        {
            TypeScriptType typeToAdd = obj;
            if (obj.IsArray)
            {
                typeToAdd = obj.GetNonCollectionElementType();
            }

            if (!IsHandled(typeToAdd))
            {
                queue.Add(typeToAdd);
            }
            foreach (TypeScriptProperty property in typeToAdd.AllMembers)
            {
                TypeScriptType propertyTypeToAdd = property.MemberType;
                if (propertyTypeToAdd.IsArray)
                {
                    propertyTypeToAdd = propertyTypeToAdd.GetNonCollectionElementType();
                }
                if (!IsHandled(propertyTypeToAdd))
                {
                    queue.Add(propertyTypeToAdd);
                }
            }
        }

        private bool IsHandled(TypeScriptType obj)
        {
            return queue.Contains(obj) || searched.Contains(obj);
        }

        private TypeScriptType[] FindRelevantDescendentTypes(TypeScriptType type)
        {
            // get all of the descendent types
            IEnumerable<TypeScriptType> descendentTypes = type.GetDescendentTypes().Where(t => !t.IsAbstract);
            
            // if it has descendent types then include this type
            IEnumerable<TypeScriptType> ownType;
            if (descendentTypes.Any() && !type.IsAbstract)
            {
                ownType = new TypeScriptType[] { type };
            }
            else
            {
                ownType = new TypeScriptType[0];
            }
            return descendentTypes.Concat(ownType).ToArray();
        }
    }
}
